﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="DefaultVirtualPathProvider.cs" company="">
//   
// </copyright>
// <summary>
//   The default virtual path provider.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Skymate.Core.IO.VirtualPath
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Web;
    using System.Web.Hosting;

    /// <summary>
    /// The default virtual path provider.
    /// </summary>
    public class DefaultVirtualPathProvider : IVirtualPathProvider
    {
        // private readonly ILogger _logger;

        /// <summary>
        /// The get directory name.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public virtual string GetDirectoryName(string virtualPath)
        {
            return Path.GetDirectoryName(virtualPath).Replace(Path.DirectorySeparatorChar, '/');
        }

        /// <summary>
        /// The list files.
        /// </summary>
        /// <param name="path">
        /// The path.
        /// </param>
        /// <returns>
        /// The <see cref="IEnumerable"/>.
        /// </returns>
        public virtual IEnumerable<string> ListFiles(string path)
        {
            return
                HostingEnvironment.VirtualPathProvider.GetDirectory(path)
                    .Files.OfType<VirtualFile>()
                    .Select(f => VirtualPathUtility.ToAppRelative(f.VirtualPath));
        }

        /// <summary>
        /// The list directories.
        /// </summary>
        /// <param name="path">
        /// The path.
        /// </param>
        /// <returns>
        /// The <see cref="IEnumerable"/>.
        /// </returns>
        public virtual IEnumerable<string> ListDirectories(string path)
        {
            return
                HostingEnvironment.VirtualPathProvider.GetDirectory(path)
                    .Directories.OfType<VirtualDirectory>()
                    .Select(d => VirtualPathUtility.ToAppRelative(d.VirtualPath));
        }

        /// <summary>
        /// The combine.
        /// </summary>
        /// <param name="paths">
        /// The paths.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public virtual string Combine(params string[] paths)
        {
            return Path.Combine(paths).Replace(Path.DirectorySeparatorChar, '/');
        }

        /// <summary>
        /// The to app relative.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public virtual string ToAppRelative(string virtualPath)
        {
            if (this.IsMalformedVirtualPath(virtualPath))
            {
                return null;
            }

            try
            {
                var result = VirtualPathUtility.ToAppRelative(virtualPath);

                // In some cases, ToAppRelative doesn't normalize the path. In those cases,
                // the path is invalid.
                // Example:
                // ApplicationPath: /Foo
                // VirtualPath    : ~/Bar/../Blah/Blah2
                // Result         : /Blah/Blah2  <= that is not an app relative path!
                if (!result.StartsWith("~/"))
                {
                    // this._logger.Information("Path '{0}' cannot be made app relative: Path returned ('{1}') is not app relative.".FormatCurrent(virtualPath, result));
                    return null;
                }

                return result;
            }
            catch (Exception e)
            {
                // The initial path might have been invalid (e.g. path indicates a path outside the application root)
                // this._logger.Information("Path '{0}' cannot be made app relative".FormatCurrent(virtualPath), e);
                return null;
            }
        }

        /// <summary>
        /// The open file.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        /// <returns>
        /// The <see cref="Stream"/>.
        /// </returns>
        public virtual Stream OpenFile(string virtualPath)
        {
            return HostingEnvironment.VirtualPathProvider.GetFile(virtualPath).Open();
        }

        /// <summary>
        /// The create text.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        /// <returns>
        /// The <see cref="StreamWriter"/>.
        /// </returns>
        public virtual StreamWriter CreateText(string virtualPath)
        {
            return File.CreateText(this.MapPath(virtualPath));
        }

        /// <summary>
        /// The create file.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        /// <returns>
        /// The <see cref="Stream"/>.
        /// </returns>
        public virtual Stream CreateFile(string virtualPath)
        {
            return File.Create(this.MapPath(virtualPath));
        }

        /// <summary>
        /// The get file last write time utc.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        /// <returns>
        /// The <see cref="DateTime"/>.
        /// </returns>
        public virtual DateTime GetFileLastWriteTimeUtc(string virtualPath)
        {
            return File.GetLastWriteTimeUtc(this.MapPath(virtualPath));
        }

        /// <summary>
        /// The get file hash.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public string GetFileHash(string virtualPath)
        {
            return this.GetFileHash(virtualPath, new[] { virtualPath });
        }

        /// <summary>
        /// The get file hash.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        /// <param name="dependencies">
        /// The dependencies.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public string GetFileHash(string virtualPath, IEnumerable<string> dependencies)
        {
            return HostingEnvironment.VirtualPathProvider.GetFileHash(virtualPath, dependencies);
        }

        /// <summary>
        /// The delete file.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        public virtual void DeleteFile(string virtualPath)
        {
            File.Delete(this.MapPath(virtualPath));
        }

        /// <summary>
        /// The map path.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public virtual string MapPath(string virtualPath)
        {
            return HostingEnvironment.MapPath(virtualPath);
        }

        /// <summary>
        /// The normalize.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public virtual string Normalize(string virtualPath)
        {
            return HostingEnvironment.VirtualPathProvider.GetFile(virtualPath).VirtualPath;
        }

        /// <summary>
        /// The file exists.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public virtual bool FileExists(string virtualPath)
        {
            return HostingEnvironment.VirtualPathProvider.FileExists(virtualPath);
        }

        /// <summary>
        /// The try file exists.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public virtual bool TryFileExists(string virtualPath)
        {
            if (this.IsMalformedVirtualPath(virtualPath))
            {
                return false;
            }

            try
            {
                return this.FileExists(virtualPath);
            }
            catch (Exception e)
            {
                // this._logger.Information("File '{0}' can not be checked for existence. Assuming doesn't exist.".FormatCurrent(virtualPath), e);
                return false;
            }
        }

        /// <summary>
        /// The directory exists.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public virtual bool DirectoryExists(string virtualPath)
        {
            return HostingEnvironment.VirtualPathProvider.DirectoryExists(virtualPath);
        }

        /// <summary>
        /// The create directory.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        public virtual void CreateDirectory(string virtualPath)
        {
            Directory.CreateDirectory(this.MapPath(virtualPath));
        }

        /// <summary>
        /// The delete directory.
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual path.
        /// </param>
        public virtual void DeleteDirectory(string virtualPath)
        {
            Directory.Delete(this.MapPath(virtualPath));
        }

        /// <summary>
        /// We want to reject path that contains ".." going outside of the application root.
        ///     ToAppRelative does that already, but we want to do the same while avoiding exceptions.
        ///     Note: This method doesn't detect all cases of malformed paths, it merely checks
        ///     for *some* cases of malformed paths, so this is not a replacement for full virtual path
        ///     verification through VirtualPathUtilty methods.
        ///     In other words, !IsMalformed does *not* imply "IsWellformed".
        /// </summary>
        /// <param name="virtualPath">
        /// The virtual Path.
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public bool IsMalformedVirtualPath(string virtualPath)
        {
            if (string.IsNullOrEmpty(virtualPath))
            {
                return true;
            }

            if (virtualPath.IndexOf("..") >= 0)
            {
                virtualPath = virtualPath.Replace(Path.DirectorySeparatorChar, '/');
                var rootPrefix = virtualPath.StartsWith("~/") ? "~/" : virtualPath.StartsWith("/") ? "/" : string.Empty;
                if (!string.IsNullOrEmpty(rootPrefix))
                {
                    var terms = virtualPath.Substring(rootPrefix.Length).Split('/');
                    var depth = 0;
                    foreach (var term in terms)
                    {
                        if (term == "..")
                        {
                            if (depth == 0)
                            {
                                // this._logger.Information("Path '{0}' cannot be made app relative: Too many '..'".FormatCurrent(virtualPath));
                                return true;
                            }

                            depth--;
                        }
                        else
                        {
                            depth++;
                        }
                    }
                }
            }

            return false;
        }
    }
}